﻿// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Collections.Generic;
using System.Transactions;

using Tierless.Infrastructure.Data;
using Tierless.Framework.Data;
using Tierless.Framework.Entities;
using Tierless.Framework.EntityTypes;
using Tierless.Framework.Constraints;

using IsolationLevel = System.Transactions.IsolationLevel;

namespace Tierless.Framework.Server.Data
{
	/// <summary>
	/// Provides an entity manager for use in retrieving and managing entities in an entity store.
	/// </summary>
	public class EntityManager : IServerEntityAccessor
	{
		/// <summary>
		/// Initializes a new instance of an EntityManager class with a default database connection.
		/// </summary>
		public EntityManager() : this(new Database()) { }

		/// <summary>
		/// Initializes a new instance of an EntityManager with the given database connection.
		/// </summary>
		/// <param name="database">The database connection to use to store and retrieve entities.</param>
		public EntityManager(Database database)
		{
			_entityProvider = new EntityProvider(database);
			_constraintEngine = new ServerConstraintEngine(this);
		}

		private EntityProvider _entityProvider;
		/// <summary>
		/// Gets the underlying entity provider used by the manager.
		/// </summary>
		public EntityProvider EntityProvider { get { return _entityProvider; } }
		
		#region Constraint Validation
		
		private ConstraintEngine _constraintEngine;

		/// <summary>
		/// Performs validation on the given entity, and any child entities, recursively.
		/// </summary>
		/// <param name="entity">The entity to be validated.</param>
		/// <returns>True if the entity, and all child entities passed validation, false otherwise.</returns>
		public bool Validate(Entity entity)
		{
			return Validate(entity, false);
		}

		/// <summary>
		/// Performs validation on the given entity, and any child entities, recursively.
		/// </summary>
		/// <param name="entity">The entity to be validated.</param>
		/// <param name="isTemporalUpdate">Indicates whether the update is changing the timeframe of the entity.</param>
		/// <returns>True if the entity, and all child entities passed validation, false otherwise.</returns>
		public bool Validate(Entity entity, bool isTemporalUpdate)
		{
			var changes = new List<EntityChange>();
			entity.Recurse
			(
				delegate(Entity ACallbackEntity) 
				{ 
					if (ACallbackEntity.IsDirty) 
						changes.Add(new EntityChange(ACallbackEntity, isTemporalUpdate, new string[0])); 
					else
					{
						var shouldValidate = false;
						foreach (var children in ACallbackEntity.Children)
						{
							if (shouldValidate)
								break;
								
							foreach (var child in children)
								if (child.IsDirty)
								{
									shouldValidate = true;
									break;
								}
						}
						
						if (shouldValidate)
							changes.Add(new EntityChange(ACallbackEntity, isTemporalUpdate, new string[0]));
					}
				}
			);
			
			var entities = new EntitySets { entity };
			entities.Normalize();

			return _constraintEngine.Execute(changes, entities);
		}

		private class ServerConstraintEngine : ConstraintEngine
		{
			public ServerConstraintEngine(EntityManager entityManager) 
				: base(ConstraintLocation.Server, entityManager)
			{ 
				_entityManager = entityManager;
			}
			
			private Dictionary<string, EntityConstraintCollection> _constraintsByEntity = new Dictionary<string, EntityConstraintCollection>();
			private object _constraintsSync = new Object();
			private EntityManager _entityManager;
			
			protected override IList<EntityConstraint> GetConstraintsForEntityType(string entityTypeName)
			{
				EntityConstraintCollection constraints;
				lock (_constraintsSync)
				{
					if (!_constraintsByEntity.TryGetValue(entityTypeName, out constraints))
					{
						constraints = new EntityConstraintCollection();
						
						foreach (var constraint in _entityManager.EntityProvider.GetEntityType(entityTypeName).Constraints)
							constraints.Add(constraint);
							
						foreach (var constraint in _entityManager.EntityProvider.GetServerEntityType(entityTypeName).Constraints)
							constraints.Add(constraint);
							
						_constraintsByEntity.Add(entityTypeName, constraints);
					}
				}
				return constraints;
			}
		}
		
		#endregion

		/// <summary>
		/// Retrieves a set of entities based on the given entity request.
		/// </summary>
		/// <param name="request">The entity request.</param>
		/// <returns>A set of entities satisfying the given entity request.</returns>
		public EntitySet Get(EntityRequest request)
		{
			if (request == null) throw new ArgumentNullException("ARequest");

			var requestById = request as EntityGetByIDRequest;
			if (requestById != null)
			{
				var entitySet = new EntitySet(requestById.EntityTypeName);
				var entity = EntityProvider.GetByID(requestById.EntityTypeName, requestById.EntityId, requestById.AsOf, request.EntityTraversal);
				if (entity != null)
					entitySet.Add(entity);
				else
					entitySet.Add(requestById.EntityId, null);
				return entitySet;
			}
			
			var requestByMasterID = request as EntityGetByMasterIDRequest;
			if (requestByMasterID != null)
			{
				var parameters = new EntityParameter[requestByMasterID.DetailColumnNames.Length];
				for (int index = 0; index < parameters.Length; index++)
					parameters[index] = new EntityParameter(requestByMasterID.EntityTypeName, requestByMasterID.DetailColumnNames[index], requestByMasterID.MasterID[index]);

				return EntityProvider.Get(new EntityQuery(new EntitySpecifier(requestByMasterID.EntityTypeName), parameters, requestByMasterID.AsOf));
			}

			var requestGet = request as EntityGetRequest;
			if (requestGet != null)
			{
				return EntityProvider.Get(requestGet.EntityTypeName, requestGet.CommandName, requestGet.Filter.ToArray(), requestGet.AsOf, requestGet.EntityTraversal);
			}

			throw new ArgumentException(String.Format("Unknown request type: '{0}'.", request.GetType().Name));
		}

		/// <summary>
		/// Retrieves a list of entity sets based on the given list of entity requests.
		/// </summary>
		/// <param name="requests">The list of entity requests.</param>
		/// <returns>A list of entity sets satisfying the given requests.</returns>
		/// <remarks>
		/// This method is equivalent to iterative invocation of the singular <c>Get</c> method.
		/// </remarks>
		public List<EntitySet> Get(EntityRequests requests)
		{
			var results = new List<EntitySet>();
			foreach (var request in requests)
				results.Add(Get(request));

			return results;
		}

		/// <summary>
		/// Saves any outstanding changes to an entity and returns whether or not the save succeeded.
		/// </summary>
		/// <param name="entity">The entity to be saved.</param>
		/// <returns>Returns true if the save was successful (i.e., no validation errors were encountered.), false otherwise.</returns>
		/// <remarks>
		/// <para>
		/// This method will return false if the save was not successful. The operation is performed within a transaction, and any violation
		/// errors cause the scope to be rolled back, undoing any changes made to the database.
		/// </para>
		/// <para>
		/// It is up to the caller to examine the violations that will be placed on the entity instance.
		/// </para>
		/// <para>
		/// If the call is successful, the entity changes will be accepted and the entity state will be set to <c>Unchanged</c>.
		/// </para>
		/// <para>
		/// If the save does not succeed, the entity will remain unaffected, leaving the outstanding changes and the modified state.
		/// </para>
		/// </remarks>
		public bool SafeSave(ref Entity entity)
		{
		    var hasViolations = Validate(entity);
			if (!entity.AnyErrors)
			{
				using (var scope = new TransactionScope())
				{
					EntityProvider.Save(ref entity);
					scope.Complete();
					return true;
				}
			}

            return false;
		}
		
		/// <summary>
		/// Saves any outstanding changes to an entity and throws an exception if the save is not successful.
		/// </summary>
		/// <param name="entity">The entity to be saved.</param>
		/// <remarks>
		/// <para>
		/// This method will throw an exception if the save is not successful. The operation is performed 
		/// within a transaction, and any violation errors cause the scope to be rolled back, undoing any 
		/// changes made to the database.
		/// </para>
		/// <para>
		/// Any violations that were encountered will be placed on the entity instance.
		/// </para>
		/// <para>
		/// If the call is successful, the entity changes will be accepted and the entity state will be set to <c>Unchanged</c>.
		/// </para>
		/// <para>
		/// If the save does not succeed, the entity will remain unaffected, leaving the outstanding changes and the modified state.
		/// </para>
		/// </remarks>
		public void Save(ref Entity entity)
		{
			if (!SafeSave(ref entity))
				throw new ConstraintException(entity.GatherViolations().CombinedErrorMessage);
		}

		/// <summary>
		/// Saves any outstanding changes on any entity in the given entity sets.
		/// </summary>
		/// <param name="entitySets">The list of entity sets containing the entities to be saved.</param>
		/// <returns>An entity save result describing the results of the save.</returns>
		/// <remarks>
		/// All the saves happen within the same transaction scope, so any error that occurs will prevent
		/// all changes from being made.
		/// </remarks>
		public EntitySaveResult Save(List<EntitySet> entitySets)
		{
			var hasViolations = false;
			var anyErrors = false;
			foreach (var entitySet in entitySets)
				foreach (var entity in entitySet)
				{
					hasViolations = hasViolations || Validate(entity);
					anyErrors = anyErrors || entity.AnyErrors;
				}
				
			if (!anyErrors)
			{
				using (var scope = new TransactionScope())
				{
					foreach (var entitySet in entitySets)
					{
						for (var i = 0; i < entitySet.Count; i++)
						{
							var entity = entitySet[i];
							try
							{
								EntityProvider.Save(ref entity);
							}
							catch
							{
								entitySet[i] = entity;
								throw;
							}
						}
					}
					scope.Complete();
				}
			}
			
			return new EntitySaveResult(!anyErrors, entitySets);
		}

		/// <summary>
		/// Deletes an entity.
		/// </summary>
		/// <param name="entityTypeName">The type of entity to be deleted.</param>
		/// <param name="entityId">The ID of the entity to be deleted.</param>
		/// <param name="asOf">The effective date of the deletion.</param>
		/// <returns>A list of constraint violations that occurred during the deletion, if any.</returns>
		public ConstraintViolations SafeDelete(string entityTypeName, EntityID entityId, DateTime asOf)
		{
			var entity = EntityProvider.GetByID(entityTypeName, entityId, asOf, true);
			if (entity == null)
			{
				var violations = new ConstraintViolations();
				violations.Add
				(
					new ConstraintViolation
					(
						"DeleteValid", 
						entityTypeName, 
						new string[] { }, 
						new string[] { }, 
						ViolationSeverity.Error, 
						String.Format
						(
							"{0} with ID {1} could not be deleted because it does not exist.", 
							entityTypeName, 
							entityId
						)
					)
				);
				return violations;
			}
				
			entity.MarkToDelete(asOf);
			SafeSave(ref entity);
			return entity.GatherViolations();
		}

		/// <summary>
		/// Deletes an entity, throwing an exception if any violations are encountered.
		/// </summary>
		/// <param name="entityTypeName">The type of the entity to be deleted.</param>
		/// <param name="entityId">The ID of the entity to be deleted.</param>
		/// <param name="asOf">The effective date for the deletion.</param>
		public void Delete(string entityTypeName, EntityID entityId, DateTime asOf)
		{
			var violations = SafeDelete(entityTypeName, entityId, asOf);
			if (violations.HasErrors())
				throw new ConstraintException(violations.CombinedErrorMessage);
		}

		/// <summary>
		/// Returns the entity type instance describing the structure of a given entity type.
		/// </summary>
		/// <param name="entityName">The name of the entity type to be described.</param>
		/// <returns>An entity type instance describing the structure of a given entity type.</returns>
		public EntityType Describe(string entityName)
		{
			return EntityProvider.GetEntityType(entityName);
		}

		#region IEntityAccessor Members
		
		/// <inheritdoc/>
		public EntitySets RequestEntity(EntityRequest request)
		{
			var requests = new EntityRequests { request };
			return RequestEntity(requests);
		}

		/// <inheritdoc/>
		public EntitySets RequestEntity(EntityRequests requests)
		{
			var entitySets = Get(requests);
			var results = new EntitySets();
			for (var index = 0; index < requests.Count; index++)
				results.Include(requests[index].EntitySetName, entitySets[index]);
			return results;
		}

		#endregion

		#region IServerEntityAccessor Members

		/// <inheritdoc/>
		public Database Database
		{
			get { return EntityProvider.Database; }
		}

		#endregion
	}
}
